From: Joseph Marrero Corchado Date: Wed, 1 Apr 2026 19:40:45 +0000 (-0400) Subject: bootconfig: Preserve extension BLS keys across staged deployments X-Git-Tag: archive/raspbian/2026.2-1+rpi1~1^2~10^2^2~2^2 X-Git-Url: https://dgit.raspbian.org/%22http://www.example.com/cgi/%22/%22http:/www.example.com/cgi/%22?a=commitdiff_plain;h=4affc1bdd5fe1582fcd23d056ae7beb7b8717324;p=ostree.git bootconfig: Preserve extension BLS keys across staged deployments When a deployment is staged via ostree_sysroot_stage_tree_with_options(), the deployment metadata is serialized to /run/ostree/staged-deployment as a GVariant. During finalization at shutdown, _ostree_sysroot_reload_staged() creates a fresh OstreeBootconfigParser and only restores the "options" key from the serialized kargs. Any additional BLS keys that were set on the bootconfig are silently dropped. The parse/write/clone paths in OstreeBootconfigParser already handle unknown keys generically (the "Write unknown fields" loop in write_at()), so keys survive direct deployments and in-memory operations. The gap is exclusively in the staged deployment roundtrip, where a fresh bootconfig is rebuilt from just the kargs strv. This matters for the upcoming bootc `loader-entries set-options-for-source` feature, which stores kernel argument ownership as extension BLS keys (e.g. `x-options-source-tuned nohz=full isolcpus=1-3`). On bootc systems with transient /etc, tools like TuneD lose track of which kargs they own because their state files are wiped on reboot. Tracking ownership directly in the BLS config on /boot solves this, but only if the keys survive staging. systemd-boot, GRUB, and zipl all ignore unknown BLS keys, so extension keys are safe. Fix this by following the same pattern used for overlay-initrds: 1. Add _ostree_bootconfig_parser_get_extra_keys_variant() which returns all non-standard BLS keys as an a{ss} GVariant. Standard keys (title, version, options, linux, initrd, devicetree) are excluded since they are rebuilt from scratch during finalization. All other keys are preserved, trusting the caller. 2. In ostree_sysroot_stage_tree_with_options(), serialize any extra keys as "bootconfig-extra" in the staged GVariant dict. Since _ostree_deployment_set_bootconfig_from_kargs() creates a fresh bootconfig with only the "options" key, the code falls back to the merge deployment's bootconfig for extra keys. This ensures keys are inherited across staged deployments without the caller needing to re-set them. 3. In _ostree_sysroot_reload_staged(), restore extra keys from the "bootconfig-extra" dict onto the deployment's bootconfig via ostree_bootconfig_parser_set(). The function is private (_ostree_ prefix) since only ostree's own staging code uses it. No new public API, no changes to .sym files, no changes to GIR or Rust bindings. Backwards compatibility: - Old ostree ignores the unknown "bootconfig-extra" key in the a{sv} dict (extension keys silently lost, same as before this patch). - New ostree gracefully handles the absence of "bootconfig-extra" in staged data written by older versions (g_variant_dict_lookup returns FALSE, no restoration attempted). Assisted-by: OpenCode (Claude claude-opus-4-6) Signed-off-by: Joseph Marrero Corchado --- diff --git a/src/libostree/ostree-bootconfig-parser-private.h b/src/libostree/ostree-bootconfig-parser-private.h index 16ccb0fc..1450fa99 100644 --- a/src/libostree/ostree-bootconfig-parser-private.h +++ b/src/libostree/ostree-bootconfig-parser-private.h @@ -8,4 +8,6 @@ G_BEGIN_DECLS const char *_ostree_bootconfig_parser_filename (OstreeBootconfigParser *self); +GVariant *_ostree_bootconfig_parser_get_extra_keys_variant (OstreeBootconfigParser *self); + G_END_DECLS diff --git a/src/libostree/ostree-bootconfig-parser.c b/src/libostree/ostree-bootconfig-parser.c index 2e08256d..4e7c823a 100644 --- a/src/libostree/ostree-bootconfig-parser.c +++ b/src/libostree/ostree-bootconfig-parser.c @@ -339,6 +339,60 @@ ostree_bootconfig_parser_write (OstreeBootconfigParser *self, GFile *output, cancellable, error); } +/* Standard BLS keys that are managed by ostree's own deployment code. + * These are rebuilt from scratch during staged deployment finalization + * (title, version, linux, initrd from the deployment; options from the + * serialized kargs), so they must NOT be duplicated into bootconfig-extra. + */ +static const char *const standard_bls_keys[] + = { "title", "version", "options", "linux", "initrd", "devicetree", NULL }; + +static gboolean +is_standard_bls_key (const char *key) +{ + for (const char *const *p = standard_bls_keys; *p != NULL; p++) + { + if (strcmp (key, *p) == 0) + return TRUE; + } + return FALSE; +} + +/** + * _ostree_bootconfig_parser_get_extra_keys_variant: + * @self: Parser + * + * Returns a GVariant of type "a{ss}" containing all bootconfig keys + * that are not part of the standard BLS set managed by ostree. These + * are extension keys set by consumers like bootc (e.g. + * "x-options-source-tuned") that need to survive the staged deployment + * serialization roundtrip. + * + * Returns: (transfer full) (nullable): A new floating GVariant, or NULL if + * there are no extra keys + */ +GVariant * +_ostree_bootconfig_parser_get_extra_keys_variant (OstreeBootconfigParser *self) +{ + g_auto (GVariantBuilder) builder = OT_VARIANT_BUILDER_INITIALIZER; + gboolean has_entries = FALSE; + + g_variant_builder_init (&builder, (GVariantType *)"a{ss}"); + + GLNX_HASH_TABLE_FOREACH_KV (self->options, const char *, k, const char *, v) + { + if (is_standard_bls_key (k)) + continue; + g_variant_builder_add (&builder, "{ss}", k, v); + has_entries = TRUE; + } + + if (!has_entries) + return NULL; + + return g_variant_builder_end (&builder); +} + static void ostree_bootconfig_parser_finalize (GObject *object) { diff --git a/src/libostree/ostree-sysroot-deploy.c b/src/libostree/ostree-sysroot-deploy.c index 1dbaaa7e..77b601ff 100644 --- a/src/libostree/ostree-sysroot-deploy.c +++ b/src/libostree/ostree-sysroot-deploy.c @@ -3884,6 +3884,33 @@ ostree_sysroot_stage_tree_with_options (OstreeSysroot *self, const char *osname, g_variant_builder_add (builder, "{sv}", "overlay-initrds", g_variant_new_strv ((const char *const *)opts->overlay_initrds, -1)); + /* Serialize any extension BLS keys (e.g. x-options-source-tuned). + * These are custom keys set by consumers like bootc and need to survive + * the staging roundtrip so they are preserved during finalization at shutdown. + * + * First check the new deployment's bootconfig (in case the caller set keys + * on it directly). If none found, fall back to the merge deployment's + * bootconfig, which carries the keys from the currently deployed BLS entry. + * This ensures that x-prefixed keys are inherited across staged deployments + * even though _ostree_deployment_set_bootconfig_from_kargs() creates a fresh + * bootconfig containing only the "options" key. + */ + { + GVariant *extra = NULL; + OstreeBootconfigParser *bootconfig = ostree_deployment_get_bootconfig (deployment); + if (bootconfig) + extra = _ostree_bootconfig_parser_get_extra_keys_variant (bootconfig); + if (!extra && merge_deployment) + { + OstreeBootconfigParser *merge_bootconfig + = ostree_deployment_get_bootconfig (merge_deployment); + if (merge_bootconfig) + extra = _ostree_bootconfig_parser_get_extra_keys_variant (merge_bootconfig); + } + if (extra) + g_variant_builder_add (builder, "{sv}", "bootconfig-extra", extra); + } + const char *parent = dirname (strdupa (_OSTREE_SYSROOT_RUNSTATE_STAGED)); if (!glnx_shutil_mkdir_p_at (AT_FDCWD, parent, 0755, cancellable, error)) return FALSE; diff --git a/src/libostree/ostree-sysroot.c b/src/libostree/ostree-sysroot.c index 282df6d1..5b837bb2 100644 --- a/src/libostree/ostree-sysroot.c +++ b/src/libostree/ostree-sysroot.c @@ -1225,6 +1225,24 @@ _ostree_sysroot_reload_staged (OstreeSysroot *self, GError **error) _ostree_deployment_set_overlay_initrds (staged, overlay_initrds); + /* Restore any extension BLS keys (e.g. x-options-source-tuned) + * that were serialized during staging. This preserves custom keys + * set by consumers like bootc through the staging roundtrip. + */ + { + g_autoptr (GVariant) bootconfig_extra = NULL; + if (g_variant_dict_lookup (staged_deployment_dict, "bootconfig-extra", "@a{ss}", + &bootconfig_extra)) + { + OstreeBootconfigParser *bootconfig = ostree_deployment_get_bootconfig (staged); + GVariantIter iter; + const char *key, *value; + g_variant_iter_init (&iter, bootconfig_extra); + while (g_variant_iter_next (&iter, "{&s&s}", &key, &value)) + ostree_bootconfig_parser_set (bootconfig, key, value); + } + } + self->staged_deployment = g_steal_pointer (&staged); self->staged_deployment_data = g_steal_pointer (&staged_deployment_data); /* We set this flag for ostree_deployment_is_staged() because that API diff --git a/tests/test-bootconfig-parser-internals.c b/tests/test-bootconfig-parser-internals.c index 00b18d4a..d84c4a3b 100644 --- a/tests/test-bootconfig-parser-internals.c +++ b/tests/test-bootconfig-parser-internals.c @@ -49,6 +49,211 @@ test_parse_tries_invalid (void) g_assert_cmpuint (done, ==, 0); } +static void +test_extra_keys_variant_empty (void) +{ + /* A bootconfig with no x-prefixed keys should return NULL */ + OstreeBootconfigParser *parser = ostree_bootconfig_parser_new (); + ostree_bootconfig_parser_set (parser, "title", "Fedora Linux 43"); + ostree_bootconfig_parser_set (parser, "version", "1"); + ostree_bootconfig_parser_set (parser, "options", "root=UUID=abc rw quiet"); + ostree_bootconfig_parser_set (parser, "linux", "/vmlinuz-6.8.0"); + ostree_bootconfig_parser_set (parser, "initrd", "/initramfs-6.8.0.img"); + + GVariant *extra = _ostree_bootconfig_parser_get_extra_keys_variant (parser); + g_assert_null (extra); + + g_object_unref (parser); +} + +static void +test_extra_keys_variant_with_extension_keys (void) +{ + /* Standard keys should be excluded, only extension keys returned */ + OstreeBootconfigParser *parser = ostree_bootconfig_parser_new (); + ostree_bootconfig_parser_set (parser, "title", "Fedora Linux 43"); + ostree_bootconfig_parser_set (parser, "options", "root=UUID=abc rw"); + ostree_bootconfig_parser_set (parser, "linux", "/vmlinuz-6.8.0"); + ostree_bootconfig_parser_set (parser, "x-options-source-tuned", "nohz=full isolcpus=1-3"); + ostree_bootconfig_parser_set (parser, "x-options-source-dracut", "rd.driver.pre=vfio-pci"); + + GVariant *extra = _ostree_bootconfig_parser_get_extra_keys_variant (parser); + g_assert_nonnull (extra); + + GVariant *extra_owned = g_variant_ref_sink (extra); + + /* Should be a{ss} with exactly 2 entries */ + g_assert_true (g_variant_is_of_type (extra_owned, G_VARIANT_TYPE ("a{ss}"))); + g_assert_cmpuint (g_variant_n_children (extra_owned), ==, 2); + + /* Verify the contents */ + GVariantIter iter; + const char *key, *val; + gboolean found_tuned = FALSE, found_dracut = FALSE; + g_variant_iter_init (&iter, extra_owned); + while (g_variant_iter_next (&iter, "{&s&s}", &key, &val)) + { + if (g_str_equal (key, "x-options-source-tuned")) + { + g_assert_cmpstr (val, ==, "nohz=full isolcpus=1-3"); + found_tuned = TRUE; + } + else if (g_str_equal (key, "x-options-source-dracut")) + { + g_assert_cmpstr (val, ==, "rd.driver.pre=vfio-pci"); + found_dracut = TRUE; + } + else + { + g_assert_not_reached (); + } + } + g_assert_true (found_tuned); + g_assert_true (found_dracut); + + g_variant_unref (extra_owned); + g_object_unref (parser); +} + +static void +test_extra_keys_variant_standard_excluded (void) +{ + /* Standard BLS keys (title, version, options, linux, initrd, devicetree) + * should be excluded. All other keys should be preserved. + */ + OstreeBootconfigParser *parser = ostree_bootconfig_parser_new (); + ostree_bootconfig_parser_set (parser, "title", "Test"); + ostree_bootconfig_parser_set (parser, "version", "1.0"); + ostree_bootconfig_parser_set (parser, "options", "root=UUID=abc"); + ostree_bootconfig_parser_set (parser, "linux", "/vmlinuz"); + ostree_bootconfig_parser_set (parser, "initrd", "/initramfs.img"); + ostree_bootconfig_parser_set (parser, "devicetree", "/dtb"); + + /* Only standard keys -- should return NULL */ + GVariant *extra = _ostree_bootconfig_parser_get_extra_keys_variant (parser); + g_assert_null (extra); + + /* Add non-standard keys -- all should be preserved */ + ostree_bootconfig_parser_set (parser, "my-custom-key", "some-value"); + ostree_bootconfig_parser_set (parser, "x-options-source-tuned", "nohz=full"); + + extra = _ostree_bootconfig_parser_get_extra_keys_variant (parser); + g_assert_nonnull (extra); + GVariant *extra_owned = g_variant_ref_sink (extra); + + g_assert_cmpuint (g_variant_n_children (extra_owned), ==, 2); + + GVariantIter iter; + const char *key, *val; + gboolean found_custom = FALSE, found_tuned = FALSE; + g_variant_iter_init (&iter, extra_owned); + while (g_variant_iter_next (&iter, "{&s&s}", &key, &val)) + { + if (g_str_equal (key, "my-custom-key")) + { + g_assert_cmpstr (val, ==, "some-value"); + found_custom = TRUE; + } + else if (g_str_equal (key, "x-options-source-tuned")) + { + g_assert_cmpstr (val, ==, "nohz=full"); + found_tuned = TRUE; + } + else + g_assert_not_reached (); + } + g_assert_true (found_custom); + g_assert_true (found_tuned); + + g_variant_unref (extra_owned); + g_object_unref (parser); +} + +static void +test_extra_keys_roundtrip (void) +{ + /* Test that extra keys can be serialized to a variant and restored */ + OstreeBootconfigParser *original = ostree_bootconfig_parser_new (); + ostree_bootconfig_parser_set (original, "options", "root=UUID=abc rw"); + ostree_bootconfig_parser_set (original, "linux", "/vmlinuz"); + ostree_bootconfig_parser_set (original, "x-options-source-tuned", "nohz=full isolcpus=1-3"); + + /* Serialize */ + GVariant *extra = _ostree_bootconfig_parser_get_extra_keys_variant (original); + g_assert_nonnull (extra); + GVariant *extra_owned = g_variant_ref_sink (extra); + + /* Create a new parser (simulating deserialization) with only standard keys */ + OstreeBootconfigParser *restored = ostree_bootconfig_parser_new (); + ostree_bootconfig_parser_set (restored, "options", "root=UUID=abc rw nohz=full isolcpus=1-3"); + + /* Restore extra keys from variant */ + GVariantIter iter; + const char *key, *value; + g_variant_iter_init (&iter, extra_owned); + while (g_variant_iter_next (&iter, "{&s&s}", &key, &value)) + ostree_bootconfig_parser_set (restored, key, value); + + /* Verify the extension key survived the roundtrip */ + g_assert_cmpstr (ostree_bootconfig_parser_get (restored, "x-options-source-tuned"), ==, + "nohz=full isolcpus=1-3"); + /* Standard keys should also be present */ + g_assert_cmpstr (ostree_bootconfig_parser_get (restored, "options"), ==, + "root=UUID=abc rw nohz=full isolcpus=1-3"); + + g_variant_unref (extra_owned); + g_object_unref (original); + g_object_unref (restored); +} + +static void +test_extra_keys_parse_write_roundtrip (void) +{ + /* Test that x-prefixed keys survive a parse -> write -> parse roundtrip + * via the BLS file format. + */ + const char *bls_content = "title Fedora Linux 43\n" + "version 6.8.0-300.fc40.x86_64\n" + "linux /vmlinuz-6.8.0\n" + "initrd /initramfs-6.8.0.img\n" + "options root=UUID=abc rw nohz=full\n" + "x-options-source-tuned nohz=full\n"; + + /* Write the BLS content to a temp file */ + g_autofree char *tmpdir = g_dir_make_tmp ("ostree-test-XXXXXX", NULL); + g_assert_nonnull (tmpdir); + g_autofree char *tmpfile = g_build_filename (tmpdir, "ostree-test.conf", NULL); + g_assert_true (g_file_set_contents (tmpfile, bls_content, -1, NULL)); + + /* Parse */ + OstreeBootconfigParser *parser = ostree_bootconfig_parser_new (); + g_assert_true (ostree_bootconfig_parser_parse_at (parser, AT_FDCWD, tmpfile, NULL, NULL)); + + /* The x-prefixed key should have been parsed */ + g_assert_cmpstr (ostree_bootconfig_parser_get (parser, "x-options-source-tuned"), ==, + "nohz=full"); + g_assert_cmpstr (ostree_bootconfig_parser_get (parser, "options"), ==, + "root=UUID=abc rw nohz=full"); + + /* Write it back out */ + g_autofree char *outfile = g_build_filename (tmpdir, "ostree-test-out.conf", NULL); + g_assert_true (ostree_bootconfig_parser_write_at (parser, AT_FDCWD, outfile, NULL, NULL)); + + /* Parse the output and verify the key survived */ + OstreeBootconfigParser *parser2 = ostree_bootconfig_parser_new (); + g_assert_true (ostree_bootconfig_parser_parse_at (parser2, AT_FDCWD, outfile, NULL, NULL)); + g_assert_cmpstr (ostree_bootconfig_parser_get (parser2, "x-options-source-tuned"), ==, + "nohz=full"); + g_assert_cmpstr (ostree_bootconfig_parser_get (parser2, "options"), ==, + "root=UUID=abc rw nohz=full"); + + g_object_unref (parser); + g_object_unref (parser2); + (void)unlink (tmpfile); + (void)unlink (outfile); + (void)rmdir (tmpdir); +} + int main (int argc, char *argv[]) { @@ -56,5 +261,13 @@ main (int argc, char *argv[]) g_test_add_func ("/bootconfig-parser/tries/valid", test_parse_tries_valid); g_test_add_func ("/bootconfig-parser/tries/invalid", test_parse_tries_invalid); + g_test_add_func ("/bootconfig-parser/extra-keys/empty", test_extra_keys_variant_empty); + g_test_add_func ("/bootconfig-parser/extra-keys/with-extension-keys", + test_extra_keys_variant_with_extension_keys); + g_test_add_func ("/bootconfig-parser/extra-keys/standard-excluded", + test_extra_keys_variant_standard_excluded); + g_test_add_func ("/bootconfig-parser/extra-keys/roundtrip", test_extra_keys_roundtrip); + g_test_add_func ("/bootconfig-parser/extra-keys/parse-write-roundtrip", + test_extra_keys_parse_write_roundtrip); return g_test_run (); }